package com.amon.amonplayer.live;

import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.util.Log;
import android.view.Surface;

import com.amon.amonplayer.LivePush;
import com.amon.amonplayer.opengl.EglHelper;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;

import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.opengles.GL;
import javax.microedition.khronos.opengles.GL10;

public abstract class BaseMediaPush implements LivePush.ConnectListener {
    private static final int AUDIO_SAMPLE_RATE = 44100;
    private static final int AUDIO_CHANNELS = 2;
    private final WeakReference<BaseMediaPush> mMediaRecorderWr = new WeakReference<>(this);
    private Surface mSurface;
    private final EGLContext mEglContext;
    private GLSurfaceView.Renderer mRender;
    private VideoRenderThread mRenderThread;
    private VideoEncoderThread mVideoEncoderThread;
    private AudioEncoderThread mAudioEncoderThread;
    private AudioRecordThread mAudioRecordThread;
    private MediaCodec mVideoCodec;
    private MediaCodec mAudioCodec;
    private LivePush mLivePush;

    public BaseMediaPush(Context context, EGLContext eglContext) {
        mEglContext = eglContext;
    }

    public void setRender(GLSurfaceView.Renderer render) {
        this.mRender = render;
        mRenderThread = new VideoRenderThread(mMediaRecorderWr);
    }

    public void initVideo(String pushUrl, int width, int height) {
        try {
            mLivePush = new LivePush(pushUrl);
            mLivePush.setOnConnectListener(this);
            mRenderThread.setSize(width, height);
            intiVideoCodec(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
            initAudioCodec(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void intiVideoCodec(String MIME_TYPE, int videoWidth, int videoHeight) throws Exception {
        videoWidth = (videoWidth % 2 == 0) ? videoWidth : videoWidth - 1;
        videoHeight = (videoHeight % 2 == 0) ? videoHeight : videoHeight - 1;

        MediaFormat videoFormat = MediaFormat.createVideoFormat(MIME_TYPE, videoWidth, videoHeight);
        videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, videoHeight * videoWidth * 4);
        videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 24);
        videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        if (Build.VERSION.SDK_INT >= 21) {
            int profile = 0;
            int level = 0;
            if (MIME_TYPE.equals(MediaFormat.MIMETYPE_VIDEO_AVC)) {
                profile = MediaCodecInfo.CodecProfileLevel.AVCProfileHigh;
                if (videoWidth * videoHeight >= 1920 * 1080) {
                    level = MediaCodecInfo.CodecProfileLevel.AVCLevel4;
                } else {
                    level = MediaCodecInfo.CodecProfileLevel.AVCLevel31;
                }
            } else if (MIME_TYPE.equals(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
                profile = MediaCodecInfo.CodecProfileLevel.HEVCProfileMain;
                if (videoWidth * videoHeight >= 1920 * 1080) {
                    level = MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel4;
                } else {
                    level = MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel31;
                }
            }
            videoFormat.setInteger(MediaFormat.KEY_PROFILE, profile);
            // API 23以后可以设置AVC的编码level，低于23设置了但不生效
            if (Build.VERSION.SDK_INT >= 23) {
                videoFormat.setInteger(MediaFormat.KEY_LEVEL, level);
            }
        }

        mVideoCodec = MediaCodec.createEncoderByType(MIME_TYPE);
        mVideoCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mSurface = mVideoCodec.createInputSurface();
        mVideoEncoderThread = new VideoEncoderThread(mMediaRecorderWr);
    }

    private void initAudioCodec(int samplesRate, int channel) throws IOException {
        String MIME_TYPE = MediaFormat.MIMETYPE_AUDIO_AAC;
        MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE, samplesRate, channel);
        audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000);
        audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
        audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, samplesRate * channel * 2);
        mAudioCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
        mAudioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mAudioEncoderThread = new AudioEncoderThread(mMediaRecorderWr);
        mAudioRecordThread = new AudioRecordThread(mMediaRecorderWr);
    }

    @Override
    public void OnConnectError(int code, String msg) {
        Log.i("TAG", "code:" + code + " msg:" + msg);
    }

    @Override
    public void OnConnectSuccess() {
        Log.i("TAG", "连接成功");
        mVideoEncoderThread.start();
        mAudioEncoderThread.start();
        mAudioRecordThread.start();
        mRenderThread.start();
    }

    public void startPush() {
        mLivePush.initConnect();
    }

    public void stopPush() {
        mAudioRecordThread.requestExit();
        mRenderThread.requestExit();
        mVideoEncoderThread.requestExit();
        mAudioEncoderThread.requestExit();
        if (mLivePush != null) {
            mLivePush.stop();
        }
    }

    private static final class VideoRenderThread extends Thread {
        private WeakReference<BaseMediaPush> mVideoRecorderWr;
        private EglHelper mEglHelper;

        private volatile boolean mShouldExit = false;
        private boolean mHasCreateContext = false;
        private boolean mHasCreateSurface = false;
        private boolean mHasSurfaceChange = false;

        private int mWidth;
        private int mHeight;

        public VideoRenderThread(WeakReference<BaseMediaPush> videoRecorderWr) {
            mVideoRecorderWr = videoRecorderWr;
            mEglHelper = new EglHelper();
        }

        @Override
        public void run() {
            try {
                while (true) {
                    if (mShouldExit) {
                        return;
                    }
                    BaseMediaPush videoRecorder = mVideoRecorderWr.get();
                    if (videoRecorder == null) {
                        return;
                    }
                    if (!mHasCreateContext) {
                        mEglHelper.initCreateEgl(videoRecorder.mSurface, videoRecorder.mEglContext);
                        mHasCreateContext = true;
                    }

                    GL egl10 = mEglHelper.getEglContext().getGL();
                    if (!mHasCreateSurface) {
                        videoRecorder.mRender.onSurfaceCreated((GL10) egl10, mEglHelper.getEGLConfig());
                        mHasCreateSurface = true;
                    }
                    if (!mHasSurfaceChange) {
                        videoRecorder.mRender.onSurfaceChanged((GL10) egl10, mWidth, mHeight);
                        mHasSurfaceChange = true;
                    }
                    videoRecorder.mRender.onDrawFrame((GL10) egl10);
                    mEglHelper.swapBuffers();
                    Thread.sleep(16);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                onDestroy();
            }
        }

        private void onDestroy() {
            mEglHelper.destroy();
        }

        private void requestExit() {
            mShouldExit = true;
        }

        public void setSize(int width, int height) {
            this.mWidth = width;
            this.mHeight = height;
        }
    }

    private static final class VideoEncoderThread extends Thread {
        private WeakReference<BaseMediaPush> mMediaPushWr;
        private final MediaCodec mVideoCodec;
        private final MediaCodec.BufferInfo mBufferInfo;
        private byte[] mVideoSps;
        private byte[] mVideoPps;
        private ByteBuffer mSpsByteBuffer;
        private long mVideoPts = 0;

        private volatile boolean mShouldExit = false;

        public VideoEncoderThread(WeakReference<BaseMediaPush> mediaPushWr) {
            mMediaPushWr = mediaPushWr;
            mVideoCodec = mMediaPushWr.get().mVideoCodec;
            mBufferInfo = new MediaCodec.BufferInfo();
        }

        @Override
        public void run() {
            try {
                mVideoCodec.start();
                while (true) {
                    if (mShouldExit) {
                        return;
                    }
                    BaseMediaPush videoRecorder = mMediaPushWr.get();
                    if (videoRecorder == null) {
                        return;
                    }
                    int outputIndex = mVideoCodec.dequeueOutputBuffer(mBufferInfo, 0);
                    if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        ByteBuffer spsByteBuffer = mVideoCodec.getOutputFormat().getByteBuffer("csd-0");
                        mVideoSps = new byte[spsByteBuffer.remaining()];
                        spsByteBuffer.get(mVideoSps, 0, mVideoSps.length);
//                        Log.i("TAG", "pps:" + byteArrToHex(mVideoSps));

                        ByteBuffer ppsByteBuffer = mVideoCodec.getOutputFormat().getByteBuffer("csd-1");
                        mVideoPps = new byte[ppsByteBuffer.remaining()];
                        ppsByteBuffer.get(mVideoPps, 0, mVideoPps.length);
//                        Log.i("TAG", "sps:" + byteArrToHex(mVideoPps));
                    } else {
                        while (outputIndex >= 0) {
                            ByteBuffer outBuffer = mVideoCodec.getOutputBuffer(outputIndex);
                            outBuffer.position(mBufferInfo.offset);
                            outBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
                            if (mVideoPts == 0) {
                                mVideoPts = mBufferInfo.presentationTimeUs;
                            }
                            mBufferInfo.presentationTimeUs -= mVideoPts;

                            if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
                                mMediaPushWr.get().mLivePush.pushSpsPps(mVideoSps,
                                        mVideoSps.length, mVideoPps, mVideoPps.length);
                            }

                            byte[] data = new byte[outBuffer.remaining()];
                            outBuffer.get(data, 0, data.length);
//                            Log.i("TAG", "video:" + byteArrToHex(data));
                            mMediaPushWr.get().mLivePush.pushVideo(data,
                                    data.length, mBufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME);

                            mVideoCodec.releaseOutputBuffer(outputIndex, false);
                            outputIndex = mVideoCodec.dequeueOutputBuffer(mBufferInfo, 0);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                onDestroy();
            }
        }

        private void onDestroy() {
            try {
                mVideoCodec.stop();
                mVideoCodec.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        private void requestExit() {
            mShouldExit = true;
        }
    }

    private static final class AudioEncoderThread extends Thread {
        private final WeakReference<BaseMediaPush> mMediaPushWr;
        private final MediaCodec mAudioCodec;
        private final MediaCodec.BufferInfo mBufferInfo;
        private long mAudioPts;

        private volatile boolean mShouldExit = false;

        public AudioEncoderThread(WeakReference<BaseMediaPush> mediaPushWr) {
            mMediaPushWr = mediaPushWr;
            mAudioCodec = mMediaPushWr.get().mAudioCodec;
            mBufferInfo = new MediaCodec.BufferInfo();
        }

        @Override
        public void run() {
            try {
                mAudioCodec.start();
                while (true) {
                    if (mShouldExit) {
                        return;
                    }
                    BaseMediaPush videoRecorder = mMediaPushWr.get();
                    if (videoRecorder == null) {
                        return;
                    }
                    int outputIndex = mAudioCodec.dequeueOutputBuffer(mBufferInfo, 0);
                    if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    } else {
                        while (outputIndex >= 0) {
                            ByteBuffer outBuffer = mAudioCodec.getOutputBuffer(outputIndex);
                            outBuffer.position(mBufferInfo.offset);
                            outBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
                            if (mAudioPts == 0) {
                                mAudioPts = mBufferInfo.presentationTimeUs;
                            }
                            mBufferInfo.presentationTimeUs -= mAudioPts;
                            byte[] data = new byte[outBuffer.remaining()];
                            outBuffer.get(data, 0, data.length);
                            mMediaPushWr.get().mLivePush.pushAudio(data, data.length);
                            mAudioCodec.releaseOutputBuffer(outputIndex, false);
                            outputIndex = mAudioCodec.dequeueOutputBuffer(mBufferInfo, 0);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                onDestroy();
            }
        }

        private void onDestroy() {
            try {
                mAudioCodec.stop();
                mAudioCodec.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        private void requestExit() {
            mShouldExit = true;
        }
    }

    private static final class AudioRecordThread extends Thread {
        private final WeakReference<BaseMediaPush> mMediaPushWr;
        private final AudioRecord mAudioRecord;
        private final MediaCodec mAudioCodec;
        private final int mMinBufferSize;
        private final byte[] mAudioData;
        private long mAudioPts;

        private volatile boolean mShouldExit = false;

        public AudioRecordThread(WeakReference<BaseMediaPush> mediaPushWr) {
            mMediaPushWr = mediaPushWr;
            mAudioCodec = mMediaPushWr.get().mAudioCodec;

            mMinBufferSize = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE, AudioFormat.CHANNEL_IN_STEREO,
                    AudioFormat.ENCODING_PCM_16BIT);
            mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                    AUDIO_SAMPLE_RATE,
                    AudioFormat.CHANNEL_IN_STEREO,
                    AudioFormat.ENCODING_PCM_16BIT,
                    mMinBufferSize);
            mAudioData = new byte[mMinBufferSize];
        }

        @Override
        public void run() {
            try {
                mAudioRecord.startRecording();
                while (!mShouldExit) {
                    mAudioRecord.read(mAudioData, 0, mMinBufferSize);
                    int inputBufferIndex = mAudioCodec.dequeueInputBuffer(0);
                    if (inputBufferIndex >= 0) {
                        ByteBuffer byteBuffer = mAudioCodec.getInputBuffer(inputBufferIndex);
                        byteBuffer.clear();
                        byteBuffer.put(mAudioData);
                        mAudioPts += (mMinBufferSize * (1.0f * 1000000 / (AUDIO_SAMPLE_RATE * AUDIO_CHANNELS * 2)));
                        mAudioCodec.queueInputBuffer(inputBufferIndex,
                                0, mMinBufferSize, mAudioPts, 0);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                onDestroy();
            }
        }

        private void onDestroy() {
            mAudioRecord.stop();
        }

        private void requestExit() {
            mShouldExit = true;
        }
    }

    private final static char[] hexArray = "0123456789ABCDEF".toCharArray();

    private static String byteArrToHex(byte... bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }
}
